/**
 * @file esp_uffs.c
 * @brief Maps uffs <-> ESP_VFS 
 * @author Brian Pugh
 */

//#define LOG_LOCAL_LEVEL 5

#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <unistd.h>
#include <dirent.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/lock.h>
#include <sys/param.h>
#include "esp_system.h"

#include "esp_uffs.h"


static const char TAG[] = "esp_uffs";
char file_absolute_path[128] = {0};


/* static alloc the memory for each partition */
#if CONFIG_USE_STATIC_MEMORY_ALLOCATOR > 0
#define PAGE_SIZE		(CONFIG_PAGE_DATA_SIZE + CONFIG_PAGE_SPARE_SIZE)
#define BLOCK_DATA_SIZE (CONFIG_PAGE_DATA_SIZE * CONFIG_PAGES_PER_BLOCK)
static int static_buffer_for_nand[UFFS_STATIC_BUFF_SIZE(CONFIG_PAGES_PER_BLOCK, PAGE_SIZE, CONFIG_TOTAL_BLOCKS) / sizeof(int)];
#endif//CONFIG_USE_STATIC_MEMORY_ALLOCATOR


/**
 * @brief uffs DIR structure
 */
typedef struct {
    DIR dir;            /*!< VFS DIR struct */
    uffs_DIR *d;        /*!< uffs DIR struct */
    struct dirent e;    /*!< Last open dirent */
    long offset;        /*!< Offset of the current dirent */
    char *path;         /*!< Requested directory name */
} vfs_uffs_dir_t;

static int     vfs_uffs_open(void* ctx, const char * path, int flags, int mode);
static ssize_t vfs_uffs_write(void* ctx, int fd, const void * data, size_t size);
static ssize_t vfs_uffs_read(void* ctx, int fd, void * dst, size_t size);
static ssize_t vfs_uffs_pwrite(void *ctx, int fd, const void *src, size_t size, off_t offset);
static ssize_t vfs_uffs_pread(void *ctx, int fd, void *dst, size_t size, off_t offset);
static int     vfs_uffs_close(void* ctx, int fd);
static off_t   vfs_uffs_lseek(void* ctx, int fd, off_t offset, int mode);
static int     vfs_uffs_stat(void* ctx, const char * path, struct stat * st);
static int     vfs_uffs_unlink(void* ctx, const char *path);
static int     vfs_uffs_rename(void* ctx, const char *src, const char *dst);
static DIR*    vfs_uffs_opendir(void* ctx, const char* name);
static int     vfs_uffs_closedir(void* ctx, DIR* pdir);
static struct  dirent* vfs_uffs_readdir(void* ctx, DIR* pdir);
static int     vfs_uffs_readdir_r(void* ctx, DIR* pdir,
                                struct dirent* entry, struct dirent** out_dirent);
static long    vfs_uffs_telldir(void* ctx, DIR* pdir);
static void    vfs_uffs_seekdir(void* ctx, DIR* pdir, long offset);
static int     vfs_uffs_mkdir(void* ctx, const char* name, mode_t mode);
static int     vfs_uffs_rmdir(void* ctx, const char* name);
static int     vfs_uffs_fsync(void* ctx, int fd);

static esp_err_t esp_uffs_init(const esp_vfs_uffs_conf_t* conf);
static esp_err_t esp_uffs_by_mount_point(const char *cfg_mount_point,int *index);
static esp_err_t esp_uffs_get_empty(int *index);
static int       esp_uffs_flags_conv(int m);
static void esp_uffs_free(esp_uffs_t ** ufs);

/* The only way in uffs to get info is via a path (lfs_stat), so it cannot
 * be done if the path isn't stored. */
static int     vfs_uffs_fstat(void* ctx, int fd, struct stat * st);


#if CONFIG_UFFS_SPIFFS_COMPAT
static void mkdirs(esp_uffs_t * uffs, const char *dir);
static void rmdirs(esp_uffs_t * uffs, const char *dir);
#endif  // CONFIG_UFFS_SPIFFS_COMPAT

static int sem_take(esp_uffs_t *uffs);
static int sem_give(esp_uffs_t *uffs);

static SemaphoreHandle_t _uffs_lock = NULL;
static esp_uffs_t * _uffs[CONFIG_UFFS_MAX_MOUNT] = { 0 };



/********************
 * Public Functions *
 ********************/

bool esp_uffs_mounted(const char* mount_point) {
    int index = -1;
    if (esp_uffs_by_mount_point(mount_point,&index) != ESP_OK)
        return false;
    else
        return true;
}

esp_err_t esp_uffs_info(const char* mount_point, size_t *total_bytes, size_t *used_bytes){
    int index= -1;
    if (esp_uffs_by_mount_point(mount_point,&index) != ESP_OK)
        return false;

    if(total_bytes) *total_bytes = uffs_space_total(mount_point); 
    if(used_bytes) *used_bytes = uffs_space_used(mount_point);

    return ESP_OK;
}

esp_err_t esp_vfs_uffs_register(const esp_vfs_uffs_conf_t * conf)
{
    assert(conf->base_path);
    const esp_vfs_t vfs = {
        .flags       = ESP_VFS_FLAG_CONTEXT_PTR,
        .write_p     = &vfs_uffs_write,
        .pwrite_p    = &vfs_uffs_pwrite,
        .lseek_p     = &vfs_uffs_lseek,
        .read_p      = &vfs_uffs_read,
        .pread_p     = &vfs_uffs_pread,
        .open_p      = &vfs_uffs_open,
        .close_p     = &vfs_uffs_close,

        .fstat_p     = &vfs_uffs_fstat,

        .stat_p      = &vfs_uffs_stat,
        .link_p      = NULL, /* Not Supported */
        .unlink_p    = &vfs_uffs_unlink,
        .rename_p    = &vfs_uffs_rename,
        .opendir_p   = &vfs_uffs_opendir,
        .closedir_p  = &vfs_uffs_closedir,
        .readdir_p   = &vfs_uffs_readdir,
        .readdir_r_p = &vfs_uffs_readdir_r,
        .seekdir_p   = &vfs_uffs_seekdir,
        .telldir_p   = &vfs_uffs_telldir,
        .mkdir_p     = &vfs_uffs_mkdir,
        .rmdir_p     = &vfs_uffs_rmdir,
        .fsync_p     = &vfs_uffs_fsync,
        .utime_p     = NULL,
    };

    esp_err_t err = esp_uffs_init(conf);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to initialize uffs");
        return err;
    }
    int index = -1;

    if (esp_uffs_by_mount_point(conf->base_path,&index) != ESP_OK) {
        ESP_LOGE(TAG, "Unable to find mount point \"%s\"", conf->base_path);
        return ESP_ERR_NOT_FOUND;
    }

    err = esp_vfs_register(conf->base_path, &vfs, _uffs[index]);
    if (err != ESP_OK) {
        esp_uffs_free(&_uffs[index]);
        ESP_LOGE(TAG, "Failed to register uffs to \"%s\"", conf->base_path);
        return err;
    }

    ESP_LOGI(TAG, "Successfully registered uffs to \"%s\"", conf->base_path);
    return ESP_OK;
}

esp_err_t esp_vfs_uffs_unregister(const char *mount_point)
{
    int index = -1;
    if (esp_uffs_by_mount_point(mount_point,&index) != ESP_OK) {
        ESP_LOGE(TAG, "uffs(%s) was never registered.",mount_point);
        return ESP_ERR_INVALID_STATE;
    }

    ESP_LOGV(TAG, "Unregistering \"%s\"",_uffs[index]->mtbl.mount);
    esp_err_t err = esp_vfs_unregister(_uffs[index]->mtbl.mount);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to unregister \"%s\"",_uffs[index]->mtbl.mount);
        return err;
    }

    uffs_UnRegisterMountTable(&_uffs[index]->mtbl);
    uffs_UnMount(_uffs[index]->mtbl.mount);

    esp_uffs_free(&_uffs[index]);
    _uffs[index] = NULL;
    return ESP_OK;
}


#if CONFIG_UFFS_HUMAN_READABLE
/**
 * @brief converts an enumerated lfs error into a string.
 * @param uffs_error The uffs error.
 */
const char * esp_uffs_errno(int uffs_errno) {
    switch(uffs_errno){
        case U_SUCC: return "U_SUCC";
        case U_FAIL: return "U_FAIL";
        default: return "U_ERR_UNDEFINED";
    }
    return "";
}
#else
#define esp_uffs_errno(x) ""
#endif

/********************
 * Static Functions *
 ********************/




/**
 * Get a mounted uffs filesystem
 * @param[in] uffs
 * @param[out] index index into _uffs
 * @return ESP_OK on success
 */
static esp_err_t esp_uffs_by_mount_point(const char *cfg_mount_point, int *index){
    if(NULL==index || NULL== cfg_mount_point) return ESP_ERR_INVALID_ARG;
    for(uint8_t i=0; i < CONFIG_UFFS_MAX_MOUNT; i++)
    {
        if (_uffs[i]) 
        {
            if(strcmp(_uffs[i]->base_path,cfg_mount_point)==0)
            {
                *index = i;
                return ESP_OK;
            }
        }
    }
    return ESP_ERR_NOT_FOUND;

}

/**
 * @brief Get the index of an unallocated uffs slot.
 * @param[out] index Indexd of free uffs slot 
 * @return ESP_OK on success
 */
static esp_err_t esp_uffs_get_empty(int *index) {
    assert(index);
    for(uint8_t i=0; i < CONFIG_UFFS_MAX_MOUNT; i++){
        if( _uffs[i] == NULL ){
            *index = i;
            return ESP_OK;
        }
    }
    ESP_LOGE(TAG, "No more free partitions available.");
    return ESP_FAIL;
}

/**
 * @brief Convert fcntl flags to uffs flags
 * @param m fcntl flags
 * @return lfs flags
 */
static int esp_uffs_flags_conv(int m) {
    int uffs_flags = 0;
    if (m == O_APPEND) {ESP_LOGV(TAG, "O_APPEND"); uffs_flags |= UO_APPEND;}
    if (m == O_RDONLY) {ESP_LOGV(TAG, "O_RDONLY"); uffs_flags |= UO_RDONLY;}
    if (m & O_WRONLY)  {ESP_LOGV(TAG, "O_WRONLY"); uffs_flags |= UO_WRONLY;}
    if (m & O_RDWR)    {ESP_LOGV(TAG, "O_RDWR");   uffs_flags |= UO_RDWR;}
    if (m & O_EXCL)    {ESP_LOGV(TAG, "O_EXCL");   uffs_flags |= UO_EXCL;}
    if (m & O_CREAT)   {ESP_LOGV(TAG, "O_CREAT");  uffs_flags |= UO_CREATE;}
    if (m & O_TRUNC)   {ESP_LOGV(TAG, "O_TRUNC");  uffs_flags |= UO_TRUNC;}
    return uffs_flags;
}


int esp_uffs_format(esp_uffs_t * uffs)
{
    uint32_t i = 0;
    int ret = 0;
    for (i = 0; i < CONFIG_TOTAL_BLOCKS; i++)
    {   
        if(uffs->mtbl.dev && uffs->mtbl.dev->ops->EraseBlock)
        {
            if (uffs->mtbl.dev->ops->EraseBlock(uffs->mtbl.dev, i))
            {
                ret = -1;
            }
        }  
    }
    uffs_format(uffs->mtbl.mount);
    return ret;
}
/**
 * @brief Free and clear a littlefs definition structure.
 * @param efs Pointer to pointer to struct. Done this way so we can also zero
 *            out the pointer.
 */
static void esp_uffs_free(esp_uffs_t ** ufs)
{
    esp_uffs_t * e = *ufs;
    if (e == NULL) return;

    if (e->mtbl.dev->attr) 
    {
        free(e->mtbl.dev->attr);
    }
    if (e->mtbl.dev) 
    {
        free(e->mtbl.dev);
    }
    if(e->lock) vSemaphoreDelete(e->lock);
    free(e);
    *ufs = NULL;
}

static URET uffs_init_device(uffs_Device *dev)
{        
	return U_SUCC;
}

static URET uffs_release_device(uffs_Device *dev)
{
	return U_SUCC;
}

/**
 * @brief Initialize and mount uffs 
 * @param[in] conf Filesystem Configuration
 * @return ESP_OK on success
 */
static esp_err_t esp_uffs_init(const esp_vfs_uffs_conf_t* conf)
{
    int index = -1;
    esp_err_t err = ESP_FAIL;
    esp_uffs_t * uffs = NULL;

    if( _uffs_lock == NULL ){
        static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
        portENTER_CRITICAL(&mux);
        if( _uffs_lock == NULL ){
            _uffs_lock = xSemaphoreCreateMutex();
            assert(_uffs_lock);
        }
        portEXIT_CRITICAL(&mux);
    }

    xSemaphoreTake(_uffs_lock, portMAX_DELAY);

    if (esp_uffs_get_empty(&index) != ESP_OK) {
        ESP_LOGE(TAG, "max mounted partitions reached");
        err = ESP_ERR_INVALID_STATE;
        goto exit;
    }
    /* Input and Environment Validation */
    if (esp_uffs_by_mount_point(conf->base_path,&index) == ESP_OK) {
        ESP_LOGE(TAG, "mount point already used");
        err = ESP_ERR_INVALID_STATE;
        goto exit;
    }

    /* Allocate Context */
    uffs = calloc(1, sizeof(esp_uffs_t));
    if (uffs == NULL) {
        ESP_LOGE(TAG, "esp_uffs could not be malloced");
        err = ESP_ERR_NO_MEM;
        goto exit;
    }

    { /* uffs Configuration */
        uffs->context = uffs;
        strlcat(uffs->base_path, conf->base_path, ESP_VFS_PATH_MAX + 1);

        strcat(uffs->uffs_base_path,uffs->base_path);
        strcat(uffs->uffs_base_path,"/");
        
        uffs->mtbl.dev = calloc(1, sizeof(uffs_Device));
        if(NULL == uffs->mtbl.dev){
            err = ESP_ERR_INVALID_ARG;
            goto exit;  
        }

    #if CONFIG_USE_STATIC_MEMORY_ALLOCATOR > 0    
        uffs_MemSetupStaticAllocator(&uffs->mtbl.dev->mem, static_buffer_for_nand, sizeof(static_buffer_for_nand));
    #else//CONFIG_USE_SYSTEM_MEMORY_ALLOCATOR    
        uffs_MemSetupSystemAllocator(&uffs->mtbl.dev->mem);
    #endif//CONFIG_USE_STATIC_MEMORY_ALLOCATOR
        uffs->mtbl.start_block = 0;
        uffs->mtbl.end_block   = CONFIG_TOTAL_BLOCKS-1;
        uffs->mtbl.mount       = uffs->uffs_base_path;

        uffs->mtbl.dev->Init    = uffs_init_device;
        uffs->mtbl.dev->Release = uffs_release_device;

        //dev attr set
        uffs->mtbl.dev->attr = calloc(1, sizeof(struct uffs_StorageAttrSt));
        if(NULL == uffs->mtbl.dev->attr){
            err = ESP_ERR_INVALID_ARG;
            goto exit;  
        }
        uffs->mtbl.dev->attr->total_blocks = CONFIG_TOTAL_BLOCKS;
        uffs->mtbl.dev->attr->page_data_size = CONFIG_PAGE_DATA_SIZE;
        uffs->mtbl.dev->attr->pages_per_block = CONFIG_PAGES_PER_BLOCK;
        uffs->mtbl.dev->attr->spare_size = CONFIG_PAGE_SPARE_SIZE;
        uffs->mtbl.dev->attr->block_status_offs = 0;//bad block marker offset in spare area   
        //
    #ifdef CONFIG_ECC_NONE
        uffs->mtbl.dev->attr->ecc_opt = UFFS_ECC_NONE;
    #elif CONFIG_USE_UFFS_ECC
        uffs->mtbl.dev->attr->ecc_opt = UFFS_ECC_SOFT;
    #elif CONFIG_USE_FLASH_CLC_ECC
        uffs->mtbl.dev->attr->ecc_opt = UFFS_ECC_HW;
    #elif CONFIG_USE_HARDWARE_ECC
        uffs->mtbl.dev->attr->ecc_opt = UFFS_ECC_HW_AUTO;
    #endif

    #ifdef CONFIG_UFFS_LAYOUT_UFFS
        uffs->mtbl.dev->attr->layout_opt = UFFS_LAYOUT_UFFS;
    #elif CONFIG_UFFS_LAYOUT_FLASH
        uffs->mtbl.dev->attr->layout_opt = UFFS_LAYOUT_FLASH;
    #endif

        uffs->mtbl.dev->attr->_private = NULL;
        // block device operations
        uffs->mtbl.dev->ops = (uffs_FlashOps *)&conf->ops;
		uffs_RegisterMountTable(&uffs->mtbl);
    }

    uffs->lock = xSemaphoreCreateRecursiveMutex();
    if (uffs->lock == NULL) {
        ESP_LOGE(TAG, "mutex lock could not be created");
        err = ESP_ERR_NO_MEM;
        goto exit;
    }
    // Mount and Error Check
    _uffs[index] = uffs;
    if(conf->dont_mount == false){
		int res = uffs_Mount(uffs->mtbl.mount);
        if (-4 == res && conf->format_if_mount_failed == true)
        {
            esp_uffs_format(uffs);
            res = uffs_Mount(uffs->mtbl.mount);
        }

        if (res != 0) {
            ESP_LOGE(TAG, "mount failed, %s (%i)", esp_uffs_errno(res), res);
            err = ESP_FAIL;
            goto exit;
        }
	    res = uffs_InitFileSystemObjects();
        if (res != 0) {
            ESP_LOGE(TAG, "uffs init filesystem object error (%d)", res);
            err = ESP_FAIL;
            goto exit;
        }
    }

    err = ESP_OK;

exit:
    if(err != ESP_OK)
    {
        if( index >= 0 ) 
        {
            esp_uffs_free(&_uffs[index]);
        }
    }
    xSemaphoreGive(_uffs_lock);
    return err;
}

/**
 * @brief
 * @parameter ufs file system context
 */
static inline int sem_take(esp_uffs_t *ufs) {
    int res;
#if LOG_LOCAL_LEVEL >= 5
    ESP_LOGV(TAG, "------------------------ Sem Taking [%s]", pcTaskGetTaskName(NULL));
#endif
    res = xSemaphoreTakeRecursive(ufs->lock, portMAX_DELAY);
#if LOG_LOCAL_LEVEL >= 5
    ESP_LOGV(TAG, "--------------------->>> Sem Taken [%s]", pcTaskGetTaskName(NULL));
#endif
    return res;
}

/**
 * @brief
 * @parameter ufs file system context
 */
static inline int sem_give(esp_uffs_t *ufs) {
#if LOG_LOCAL_LEVEL >= 5
    ESP_LOGV(TAG, "---------------------<<< Sem Give [%s]", pcTaskGetTaskName(NULL));
#endif
    return xSemaphoreGiveRecursive(ufs->lock);
}


/*** Filesystem Hooks ***/

static int vfs_uffs_open(void* ctx, const char * path, int flags, int mode) {
    /* Note: mode is currently unused */
    int fd=-1, uffs_flags;
    esp_uffs_t *uffs = (esp_uffs_t *)ctx;

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,path);

    assert(path);

    ESP_LOGV(TAG, "Opening %s", file_absolute_path);

    /* Convert flags to lfs flags */
    uffs_flags = esp_uffs_flags_conv(flags);

    /* Get a FD */
    sem_take(uffs);

#if CONFIG_UFFS_SPIFFS_COMPAT
    /* Create all parent directories (if necessary) */
    ESP_LOGV(TAG, "uffs_SPIFFS_COMPAT attempting to create all directories for %s", path);
    mkdirs(uffs, file_absolute_path);
#endif  // CONFIG_UFFS_SPIFFS_COMPAT

    /* Open File */
    fd = uffs_open(file_absolute_path, uffs_flags);
    if( fd < 0 ) {
        sem_give(uffs);

        ESP_LOGV(TAG, "Failed to open file %s.",file_absolute_path);

        return ESP_ERR_INVALID_STATE;
    }

    sem_give(uffs);
    ESP_LOGV(TAG, "Done opening %s", file_absolute_path);
    return fd;
}

static ssize_t vfs_uffs_write(void* ctx, int fd, const void * data, size_t size) {
    esp_uffs_t * ufs = (esp_uffs_t *)ctx;
    ssize_t res;
    sem_take(ufs);

    res = uffs_write(fd, data, size);
    sem_give(ufs);

    if(res < 0){
        errno = -res;

        ESP_LOGV(TAG, "Failed to write FD %d;Error %s (%d)",
                fd, esp_uffs_errno(res), res);

        return res;
    }

    return res;
}

static ssize_t vfs_uffs_read(void* ctx, int fd, void * dst, size_t size) {
    esp_uffs_t * ufs = (esp_uffs_t *)ctx;
    ssize_t res;
    sem_take(ufs);

    res = uffs_read(fd, dst, size);
    sem_give(ufs);
    if(res < 0){
        errno = -res;
        ESP_LOGV(TAG, "Failed to read file FD %d; Error %s (%d)",
                fd, esp_uffs_errno(res), res);

        return res;
    }

    return res;
}

static ssize_t vfs_uffs_pwrite(void *ctx, int fd, const void *src, size_t size, off_t offset)
{
    esp_uffs_t *uffs = (esp_uffs_t *)ctx;
    ssize_t res, save_res;

    sem_take(uffs);
    off_t old_offset = uffs_seek(fd, 0, SEEK_CUR);
    if (old_offset < (off_t)0)
    {
        res = old_offset;
        goto exit;
    }
    /* Set to wanted position.  */
    res = uffs_seek(fd, offset, SEEK_SET);
    if (res < (off_t)0)
        goto exit;

    /* Write out the data.  */
    res = uffs_write(fd, src, size);

    /* Now we have to restore the position.  If this fails we have to
     return this as an error. But if the writing also failed we
     return writing error.  */
    save_res = uffs_seek(fd, old_offset, SEEK_SET);
    if (res >= (ssize_t)0 && save_res < (off_t)0)
    {
            res = save_res;
    }
    sem_give(uffs);

exit:
    if (res < 0)
    {
        errno = -res;

        ESP_LOGV(TAG, "Failed to write FD %d; Error %s (%d)",
                fd, esp_uffs_errno(res), res);
        return -1;
    }

    return res;
}

static ssize_t vfs_uffs_pread(void *ctx, int fd, void *dst, size_t size, off_t offset)
{
    esp_uffs_t *uffs = (esp_uffs_t *)ctx;
    ssize_t res, save_res;

    sem_take(uffs);

    off_t old_offset = uffs_seek(fd, 0, SEEK_CUR);
    if (old_offset < (off_t)0)
    {
        res = old_offset;
        goto exit;
    }

    /* Set to wanted position.  */
    res = uffs_seek(fd, offset, SEEK_SET);
    if (res < (off_t)0)
        goto exit;

    /* Read the data.  */
    res = uffs_read(fd, dst, size);

    /* Now we have to restore the position.  If this fails we have to
     return this as an error. But if the reading also failed we
     return reading error.  */
    save_res = uffs_seek(fd, old_offset, SEEK_SET);
    if (res >= (ssize_t)0 && save_res < (off_t)0)
    {
            res = save_res;
    }
    sem_give(uffs);

exit:
    if (res < 0)
    {
        errno = -res;

        ESP_LOGV(TAG, "Failed to read file FD %d; Error %s (%d)",
                fd,esp_uffs_errno(res), res);

        return -1;
    }

    return res;
}

static int vfs_uffs_close(void* ctx, int fd) {
    // TODO update mtime on close? SPIFFS doesn't do this
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    int res;
    sem_take(uffs);

    res = uffs_close(fd);
    if(res < 0){
        errno = -res;
        sem_give(uffs);

        ESP_LOGV(TAG, "Failed to close file FD %d; Error %s (%d)",
                fd, esp_uffs_errno(res), res);

        return res;
    }
    sem_give(uffs);
    return res;
}

static off_t vfs_uffs_lseek(void* ctx, int fd, off_t offset, int mode) {
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    off_t res;

    int whence = mode;

    switch (mode) {
        case SEEK_SET: break;
        case SEEK_CUR: break;
        case SEEK_END: break;
        default: 
            ESP_LOGE(TAG, "Invalid mode");
            return -1;
    }

    sem_take(uffs);

    res = uffs_seek(fd, offset, whence);
    sem_give(uffs);

    if(res < 0){
        errno = -res;
        ESP_LOGV(TAG, "Failed to seek file fd %d to offset %08x. Error %s (%d)",
                fd, (unsigned int)offset, esp_uffs_errno(res), (unsigned int)res);

        return res;
    }

    return res;
}

static int vfs_uffs_fsync(void* ctx, int fd)
{
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    ssize_t res;

    sem_take(uffs);
    res = uffs_flush(fd);
    sem_give(uffs);
    if(res < 0){
        errno = -res;

        ESP_LOGV(TAG, "Failed to sync file FD %d; Error %s (%d)",
                fd, esp_uffs_errno(res), res);

        return res;
    }

    return res;
}

static int vfs_uffs_fstat(void* ctx, int fd, struct stat * st) {
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    struct uffs_stat info;
    int res;

    memset(st, 0, sizeof(struct stat));
    st->st_blksize = uffs->mtbl.dev->attr->page_data_size * uffs->mtbl.dev->attr->pages_per_block ;

    sem_take(uffs);

    res = uffs_fstat(fd,&info);
    if (res < 0) {
        errno = -res;
        sem_give(uffs);
        ESP_LOGV(TAG, "Failed to stat file FD %d; Error %s (%d)",
                fd, esp_uffs_errno(res), res);
        return res;
    }

#ifdef CONFIG_UFFS_USE_MTIME 
    st->st_mtime = info.st_mtime;
#endif

    sem_give(uffs);
    st->st_size = info.st_size;
    st->st_mode = info.st_mode;
    return 0;
}

static int vfs_uffs_stat(void* ctx, const char * path, struct stat * st) {
    assert(path);
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    struct uffs_stat info;
    int res;

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,path);

    memset(st, 0, sizeof(struct stat));
    st->st_blksize = uffs->mtbl.dev->attr->page_data_size * uffs->mtbl.dev->attr->pages_per_block ;

    sem_take(uffs);
    res = uffs_lstat(file_absolute_path, &info);
    if (res < 0) {
        errno = -res;
        sem_give(uffs);
        /* Not strictly an error, since stat can be used to check
         * if a file exists */
        ESP_LOGV(TAG, "Failed to stat path \"%s\". Error %s (%d)",
                file_absolute_path, esp_uffs_errno(res), res);
        return res;
    }
#ifdef CONFIG_UFFS_USE_MTIME 
    st->st_mtime = info.st_mtime;
#endif
    sem_give(uffs);
    st->st_size = info.st_size;
    st->st_mode = info.st_mode;
    return 0;
}

static int vfs_uffs_unlink(void* ctx, const char *path) {
#define fail_str_1 "Failed to unlink path \"%s\"."
    assert(path);
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    int res;

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,path);
    sem_take(uffs);

    res = uffs_remove(file_absolute_path);
    if (res < 0) {
        errno = -res;
        sem_give(uffs);
        ESP_LOGV(TAG, fail_str_1 " Error %s (%d)",
                file_absolute_path, esp_uffs_errno(res), res);
        return res;
    }

#if CONFIG_UFFS_SPIFFS_COMPAT
    /* Attempt to delete all parent directories that are empty */
    rmdirs(uffs, file_absolute_path);
#endif  // CONFIG_UFFS_SPIFFS_COMPAT

    sem_give(uffs);

    return 0;
#undef fail_str_1
}

static int vfs_uffs_rename(void* ctx, const char *src, const char *dst) {
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    int res;
    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,src);

    char file_absolute_path1[128] = {0};
    strcat(file_absolute_path1,uffs->uffs_base_path);
    strcat(file_absolute_path1,dst);
    sem_take(uffs);

    res = uffs_rename(file_absolute_path, file_absolute_path1);
    sem_give(uffs);
    if (res < 0) {
        errno = -res;
        ESP_LOGE(TAG, "Failed to rename \"%s\" -> \"%s\". Error %s (%d)",
                src, dst, esp_uffs_errno(res), res);
        return res;
    }

    return 0;
}

static DIR* vfs_uffs_opendir(void* ctx, const char* name) {
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    vfs_uffs_dir_t *dir = NULL;

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,name);

    dir = calloc(1, sizeof(vfs_uffs_dir_t));
    if( dir == NULL ) {
        ESP_LOGE(TAG, "dir struct could not be malloced");
        goto exit;
    }

    dir->path = strdup(file_absolute_path);
    if(dir->path == NULL){
        ESP_LOGE(TAG, "dir path name could not be malloced");
        goto exit;
    }

    sem_take(uffs);
    dir->d = uffs_opendir(dir->path);
    sem_give(uffs);
    if (dir->d  == NULL) {   
        ESP_LOGV(TAG, "Failed to opendir \"%s\". Error", dir->path);

        goto exit;
    }

    return (DIR *)dir;

exit:
    return NULL;
}

static int vfs_uffs_closedir(void* ctx, DIR* pdir) {
    assert(pdir);
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    vfs_uffs_dir_t * dir = (vfs_uffs_dir_t *) pdir;
    int res;

    sem_take(uffs);
    res = uffs_closedir(dir->d);
    sem_give(uffs);
    if (res < 0) {
        errno = -res;     
        ESP_LOGV(TAG, "Failed to closedir \"%s\". Error %s (%d)",
                dir->path, esp_uffs_errno(res), res);

        return res;
    }

    free(dir);
    return 0;
}

static struct dirent* vfs_uffs_readdir(void* ctx, DIR* pdir) {
    assert(pdir);
    vfs_uffs_dir_t * dir = (vfs_uffs_dir_t *) pdir;
    int res;
    struct dirent* out_dirent;

    res = vfs_uffs_readdir_r(ctx, pdir, &dir->e, &out_dirent);
    if (res != 0) return NULL;
    return out_dirent;
}

static int vfs_uffs_readdir_r(void* ctx, DIR* pdir,
        struct dirent* entry, struct dirent** out_dirent) {
    assert(pdir);
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    vfs_uffs_dir_t * dir = (vfs_uffs_dir_t *) pdir;
    struct uffs_dirent *udirent = NULL;
    
    sem_take(uffs);

    udirent = uffs_readdir(dir->d);

    sem_give(uffs);
    if (udirent == NULL) {
        ESP_LOGV(TAG, "Failed to readdir \"%s\". Error",dir->path);
        return -1;
    }

    ESP_LOGV(TAG, "read size %d named \"%s\"",udirent->d_reclen, udirent->d_name);
    entry->d_ino = udirent->d_ino ;
    entry->d_type = udirent->d_type;
    strncpy(entry->d_name, udirent->d_name, sizeof(entry->d_name));
    *out_dirent = entry;

    dir->offset++;

    return 0;
}

static long vfs_uffs_telldir(void* ctx, DIR* pdir) {
    assert(pdir);
    vfs_uffs_dir_t * dir = (vfs_uffs_dir_t *) pdir;
    return dir->offset;
}

static void vfs_uffs_seekdir(void* ctx, DIR* pdir, long offset) {
    assert(pdir);
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    vfs_uffs_dir_t * dir = (vfs_uffs_dir_t *) pdir;
    int res;

    if (offset < dir->offset) {
        /* close and re-open dir to rewind to beginning */
        sem_take(uffs);
        uffs_rewinddir(dir->d);
        sem_give(uffs);
        dir->offset = 0;
    }

    while(dir->offset < offset){
        struct dirent *out_dirent;
        res = vfs_uffs_readdir_r(ctx, pdir, &dir->e, &out_dirent);
        if( res != 0 ){
            ESP_LOGE(TAG, "Error readdir_r");
            return;
        }
    }
}

static int vfs_uffs_mkdir(void* ctx, const char* name, mode_t mode) {
    /* Note: mode is currently unused */
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    int res;
    ESP_LOGV(TAG, "mkdir \"%s\"", name);

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,name);

    sem_take(uffs);
    res = uffs_mkdir(file_absolute_path);
    sem_give(uffs);
    if (res < 0) {
        errno = -res;
        ESP_LOGV(TAG, "Failed to mkdir \"%s\". Error %s (%d)",
                file_absolute_path, esp_uffs_errno(res), res);
        return res;
    }
    return 0;
}

static int vfs_uffs_rmdir(void* ctx, const char* name) {
    esp_uffs_t * uffs = (esp_uffs_t *)ctx;
    int res;

    memset(file_absolute_path,0,sizeof(file_absolute_path));
    strcat(file_absolute_path,uffs->uffs_base_path);
    strcat(file_absolute_path,name);
    /* Error Checking */
    sem_take(uffs);
    /* Unlink the dir */
    res = uffs_remove(file_absolute_path);
    sem_give(uffs);
    if ( res < 0) {
        errno = -res;
        ESP_LOGV(TAG, "Failed to unlink path \"%s\". Error %s (%d)",
                file_absolute_path, esp_uffs_errno(res), res);
        return -1;
    }

    return 0;
}

#if CONFIG_UFFS_SPIFFS_COMPAT
/**
 * @brief Recursively make all parent directories for a file.
 * @param[in] dir Path of directories to make up to. The last element
 * of the path is assumed to be the file and IS NOT created.
 *   e.g.
 *       "foo/bar/baz"
 *   will create directories "foo" and "bar"
 */
static void mkdirs(esp_uffs_t * uffs, const char *dir) {
    char tmp[CONFIG_UFFS_OBJ_NAME_LEN];
    char *p = NULL;

    strlcpy(tmp, dir, sizeof(tmp));
    for(p = tmp + 1; *p; p++) {
        if(*p == '/') {
            *p = '\0';
            vfs_uffs_mkdir((void*)uffs, tmp, S_IRWXU);
            *p = '/';
        }
    }
}

/**
 * @brief Recursively attempt to delete all empty directories for a file.
 * @param[in] dir Path of directories to delete. The last element of the path
 * is assumed to be the file and IS NOT deleted.
 *   e.g.
 *       "foo/bar/baz"
 *   will attempt to delete directories (in order):
 *       1. "foo/bar/baz"
 *       2. "foo/bar"
 *       3. "foo"
 */

static void rmdirs(esp_uffs_t * uffs, const char *dir) {
    char tmp[CONFIG_uffs_OBJ_NAME_LEN];
    char *p = NULL;

    strlcpy(tmp, dir, sizeof(tmp));
    for(p = tmp + strlen(tmp) - 1; p != tmp; p--) {
        if(*p == '/') {
            *p = '\0';
            vfs_uffs_rmdir((void*)uffs, tmp);
            *p = '/';
        }
    }
}

#endif  // CONFIG_UFFS_SPIFFS_COMPAT
